﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using FastReflectionLib;
using Fuse8.ContentManagementFramework.Domain;
using Fuse8.DomainFramework.DataAccess.SqlDatabase;
using Fuse8.DomainFramework.Domain;

namespace Fuse8.ContentManagementFramework.DataAccess.SqlAccess.Mappers
{
    internal class CmsBaseSqlEntityDeepDataMapper<TEntity, TKey>: BaseSqlEntityDataMapper<TEntity, TKey> where TEntity : class, IEntityObject<TKey>, new() where TKey : IComparable<TKey>, IEquatable<TKey>
    {
		private readonly SqlMultiMapperSettings<TEntity> _settings;
		private readonly Dictionary<string, PropertyInfo> _properties = new Dictionary<string, PropertyInfo>();
		private static readonly Dictionary<string, List<string>> _readerColumns = new Dictionary<string, List<string>>();
		private static readonly ConditionalWeakTable<IEntityObject, Dictionary<Type, List<object>>> _loadedEntities = new ConditionalWeakTable<IEntityObject, Dictionary<Type, List<object>>>();

        internal  SqlMultiMapperSettings<TEntity> Settings
        {
            get { return _settings; }
        }

        public CmsBaseSqlEntityDeepDataMapper() {}

        public CmsBaseSqlEntityDeepDataMapper(IList<string> ignoreToProperties, IList<string> ignoreFromProperties, SqlMultiMapperSettings<TEntity> settings = null) : base(ignoreToProperties, ignoreFromProperties)
        {
            _settings = settings;
        }

		internal bool IsMultiPropertiesAssigned<TParameter>()
		{
			return _settings != null && _settings.IsRegisteredForParameter<TParameter>();
		}

		internal static List<object> GetEntitesOfType(TEntity stepEntity, Type type)
		{
			Dictionary<Type, List<object>> data;
			_loadedEntities.TryGetValue(stepEntity, out data);
			if (data == null)
			{
				data = new Dictionary<Type, List<object>>();
				_loadedEntities.Add(stepEntity, data);
			}
			if (!data.ContainsKey(type))
			{
				data[type] = new List<object>();
			}

			return data[type];
		}

		#region BaseSqlEntityDataMapper override
		public override TEntity MapFromMulti(string sp, SqlDataReader reader, TEntity stepEntity, int index)
		{
			if (_settings == null)
			{
				throw new NullReferenceException("Mapper configuration for multi reader is not provded.");
			}

            if (index == 0 && _settings.FirstSetIsEntity)
            {
				stepEntity = new TEntity();
				MapFromReader(stepEntity, reader, sp, index, stepEntity);
				GetEntitesOfType(stepEntity, typeof(TEntity)).Add(stepEntity);
                return stepEntity;
            }

			if (stepEntity == null)
			{
				stepEntity = new TEntity();
				GetEntitesOfType(stepEntity, typeof(TEntity)).Add(stepEntity);
			}

            //TODO++
            var propertyMapping = _settings.GetProperty(sp, index);
            if (propertyMapping == null)
            {
                throw new ArgumentOutOfRangeException(
                    string.Format("Mapper configuration for multi reader is not provded for {0} index.", index));
            }
            var property = propertyMapping.Property;

            var elementType = !property.PropertyType.IsEnumerable()
                                  ? property.PropertyType
                                  : property.PropertyType.GetGenericArguments()[0];
            var result = !elementType.IsSimpleType()
                             ? MapFromReader(elementType, reader, sp, index, stepEntity)
                             : Converter.ChangeType(reader.GetSqlValue(0).ToString(), typeof(string), elementType);
            GetEntitesOfType(stepEntity, elementType).Add(result);

            var data = GetEntitesOfType(stepEntity, propertyMapping.ListEntityType);

		    object entityForMatching = GetEntityForMatching(stepEntity);

		    Func<object, bool> predicate = p => propertyMapping.Matching(result, p, entityForMatching);

		    foreach (var entity in data.Where(predicate))
            {
                SetProperty(entity, result, property);

                if (propertyMapping.BackProperty != null)
                {
                    SetProperty(result, entity, propertyMapping.BackProperty);
                }
            }

		    return stepEntity;
		}

        protected virtual object GetEntityForMatching(TEntity stepEntity)
        {
            return stepEntity;
        }

        private static void SetProperty(object entity, object value, PropertyInfo property)
    	{
    		if (!property.PropertyType.IsEnumerable())
    		{
    			property.FastSetValue(entity, value);
    		}
    		else
    		{
    			var collection = property.FastGetValue(entity) as IList;
    			var elementType = property.PropertyType.GetGenericArguments()[0];
    			if (collection == null)
    			{
    				collection = (IList) Activator.CreateInstance(typeof (List<>).MakeGenericType(elementType));
    				property.FastSetValue(entity, collection);
    			}
    			collection.Add(value);
    		}
    	}

    	public override TEntity MapFrom(string sp, SqlDataReader reader)
		{
			return MapFromReader<TEntity>(reader, sp, null);
		}

		#endregion
        
		protected virtual T MapFromReader<T> (SqlDataReader reader, string sp, int? index, TEntity stepEntity = null)
		{
			return (T)MapFromReader(typeof(T), reader, sp, index);
		}

        protected virtual object MapFromReader(Type type, SqlDataReader reader, string sp, int? index, TEntity stepEntity = null)
		{
			var result = Activator.CreateInstance(type);
            MapFromReader(result, reader, sp, index, stepEntity);
			return result;
		}

        protected virtual void MapFromReader(object entity, SqlDataReader reader, string sp, int? index, TEntity stepEntity = null)
        {
        	var createdEntities = new List<object> {entity};
			foreach (var fieldName in GetReaderColumns(GetKey(sp, index), reader))
			{
				var value = reader[fieldName].ToString();
                SetValue(fieldName, entity, value == "" ? null : value, sp, stepEntity, createdEntities);
			}        	
		}

        protected virtual void SetValue(string name, object obj, object value, string sp, TEntity stepEntity, List<object> createdEntities)
		{
			var propertyName = !name.Contains("_") ? name : name.Substring(0, name.IndexOf("_"));
			var propertyInfo = GetProperty(obj, propertyName);
            var objDictionary = obj as IGenericCmsEntity;
			if (propertyInfo == null)
			{
                if (objDictionary == null)
                {
                    var message =
                        String.Format("Wrong property name {0} for type {1}. Please check Stored Procedure '{2}'",
                                      propertyName, obj.GetType().Name, sp);
                    Trace.WriteLine(message);
                    return;
                }
			}

		    if (!name.Contains("_") || objDictionary != null)
			{
				try
				{
                    if (propertyInfo != null)
                    {
                        if (propertyInfo.CanWrite)
                        {
                            propertyInfo.FastSetValue(obj, Converter.ChangeType(value, propertyInfo.PropertyType));
                        }
                    }

                    if (objDictionary != null)
                    {
                        objDictionary.Add(name, Converter.ObjectToString(value));
                    }
				}
				catch (Exception e)
				{
					throw new Exception(string.Format("Type: [{3}]; Property: [{0}]; Value : [{1}]; Exception : {2}", propertyInfo.Name, value, e.Message, obj.GetType().Name), e);
				}
			}
			else
			{
				var propertyObject = propertyInfo.FastGetValue(obj);
				var subName = name.Substring(name.IndexOf("_") + 1);
				if (propertyObject == null && subName.ToLower() == "id" && value != null)
				{
					propertyObject = Activator.CreateInstance(propertyInfo.PropertyType);
					createdEntities.Add(propertyObject);
                    if (stepEntity != null)
                    {
                        GetEntitesOfType(stepEntity, propertyInfo.PropertyType).Add(propertyObject);
                    }
					propertyInfo.FastSetValue(obj, propertyObject);
				}
				if (propertyObject != null)
				{
					SetValue(subName, propertyObject, value, sp, stepEntity, createdEntities);
				}
			}
		}

		private PropertyInfo GetProperty (object obj, string propertyName)
		{
			var cacheKey = string.Format("{0}_{1}", obj.GetType().FullName, propertyName);
			return _properties.ContainsKey(cacheKey) ? _properties[cacheKey] : (_properties[cacheKey] = obj.GetType().GetProperties().FirstOrDefault(p => p.Name == propertyName));
		}

		private static IEnumerable<string> GetReaderColumns(string key, SqlDataReader reader)
		{
			if (_readerColumns.ContainsKey(key))
			{
				return _readerColumns[key];
			}
			return _readerColumns[key] = Enumerable.Range(0, reader.FieldCount).Select(reader.GetName).OrderByDescending(p => p.ToLower() == "id" || p.EndsWith("_id", true, CultureInfo.InvariantCulture)).ThenBy(p => p.Count(q => q == '_')).ToList();
		}

		private static string GetKey(string sp, int? index)
		{
			return String.Format("{0}_{1}", sp, index);
		}
         
    }

    internal class SqlMultiMapperSettings<TEntity>
    {
    public bool FirstSetIsEntity { get; set; }

        #region Private properties

		private readonly Dictionary<string, MappingProperty> _properties = new Dictionary<string, MappingProperty>();

		#endregion

        public void AddPropertyIndex<TParameter>(int index, Expression<Func<TEntity, object>> expression)
        {
        	AddPropertyIndex<TParameter>(index, expression.Body);
        }

		private void AddPropertyIndex<TParameter>(int index, Expression expression)
		{
			switch (expression.NodeType)
			{
				case ExpressionType.Call:
					var callExpression = (MethodCallExpression) expression;
					if (!SqlMultiMapperSettings.MultiMapMethodInfos.Value.Contains(callExpression.Method.GetGenericMethodDefinition().MethodHandle.Value))
					{
						throw new ArgumentOutOfRangeException("expression");
					}
			        var args = new List<Expression>(callExpression.Arguments);
			        args[0] = Expression.Convert(Expression.Constant(null), typeof (IEntityObject));

                    var f = ((UnaryExpression) callExpression.Arguments.Last()).Operand;

                    var parameters = ((IEnumerable<ParameterExpression>)f.GetType().GetProperty("Parameters").FastGetValue(f)).ToList();
                    parameters.Add((ParameterExpression)callExpression.Arguments[0]);
			        var body = (Expression) f.GetType().GetProperty("Body").FastGetValue(f);

			        var lambda = Expression.Lambda(body, parameters);

			        var objectParameters = parameters.Select(x => Expression.Parameter(typeof (object), x.Name)).ToArray();
                                  
			        var convertedParameters = parameters.Zip(objectParameters, (a, b) => Expression.Convert(b, a.Type)).ToList();

			        var keyMatching = Expression.Lambda<Func<object, object, object, bool>>(
			            Expression.Invoke(lambda, convertedParameters),
			            objectParameters).Compile();

					var listEntityType = callExpression.Method.GetGenericArguments()[1];

                    switch (args.Count)
			        {
                        case 3:
                            AddReader<TParameter>(callExpression.Arguments[1], Expression.Constant(null), keyMatching, index, listEntityType);
                            break;
                        case 4:
							AddReader<TParameter>(callExpression.Arguments[1], args[2], keyMatching, index, listEntityType);
                            break;
			            default:
                            throw new ArgumentOutOfRangeException("expression"); 
			        }
					break;
				default:
					throw new ArgumentOutOfRangeException("expression");
			}
		}

		public void AddProperties<TParameter>(Expression<Func<TEntity, object>> propertyExpression, int startIndex = 0)
		{
			if (propertyExpression.Body.NodeType != ExpressionType.New)
			{
				throw new ArgumentOutOfRangeException("propertyExpression");
			}
			var arguments = ((NewExpression) propertyExpression.Body).Arguments;
			for (var i = 0; i < arguments.Count; i++)
			{
				AddPropertyIndex<TParameter>(i + startIndex, arguments[i]);
			}
		}

		internal void SetProperty(string sp, int index, MappingProperty property)
        {
			_properties[GetKey(sp, index)] = property;
        }		
        
        internal MappingProperty GetProperty(string sp, int index)
        {
            var key = GetKey(sp, index);

            return _properties.ContainsKey(key) ? _properties[key] : null;
        }

        internal bool IsRegisteredForParameter<TParameter>()
		{
			var sp = GetCommandName<TEntity, TParameter>();
			return _properties.Any(p => p.Key.StartsWith(GetKey(sp, null)));
		}

        private string GetCommandName<T, T1>()
        {
            return string.Format("cms_sp_{0}{1}", typeof (T).Name, typeof (T1).Name);
        }

        private void AddReader<TParameter>(Expression propertyExpression, Expression backPropertyExpression, Func<object, object, object, bool> keyMatching, int index, Type listEntityType)
		{
			_properties[GetKey(GetCommandName<TEntity, TParameter>(), index)] = new MappingProperty
			                      	{
										Property = ((LambdaExpression)((UnaryExpression)propertyExpression).Operand).Body.PropertyInfo(),
										BackProperty = backPropertyExpression.NodeType == ExpressionType.Constant ? null : ((LambdaExpression)((UnaryExpression)backPropertyExpression).Operand).Body.PropertyInfo(),
			                      		Matching = keyMatching,
										ListEntityType = listEntityType
			                      	};
		}

		private static string GetKey (string sp, int? index)
		{
			return String.Format("{0}_{1}", sp, index);
		}        
    }

    internal class SqlMultiMapperSettings
    {
        public static readonly Lazy<IEnumerable<IntPtr>> MultiMapMethodInfos = new Lazy<IEnumerable<IntPtr>>(() =>
        {
            var expressions = new Expression<Func<IEntityObject, object>>[]{
               p => p.MultiMap<object, object>(x => new object[0], x => new { }, null),
               p => p.MultiMap<object, object>(x => new { }, x => new { }, null),
               p => p.MultiMap<object, object>(x => new object[0], x => new object[0], null),
               p => p.MultiMap<object, object>(x => new { }, x => new object[0], null),
               p => p.MultiMap<object, object>(x => new { }, null),
               p => p.MultiMap<object, object>(x => new object[0], null),
            };
            return
                expressions.Select(expression => ((MethodCallExpression)expression.Body).Method.GetGenericMethodDefinition().MethodHandle.Value).ToArray();
        });
    }

    internal class MappingProperty
	{
		public PropertyInfo Property { get; set; }
		public PropertyInfo BackProperty { get; set; }
        public Func<object, object, object, bool> Matching { get; set; }
		public Type ListEntityType { get; set; }
	}

    internal static class EntityObjectExtensions
	{
        public static Func<object, object, object, bool> MultiMap<TEntity, TListEntity>(this IEntityObject obj, Expression<Func<TListEntity, IEnumerable<TEntity>>> property, Expression<Func<TEntity, TListEntity>> backProperty, Expression<Func<TEntity, TListEntity, bool>> keyMatching)
		{
            return null;
		}

        public static Func<object, object, bool> MultiMap<TEntity, TListEntity>(this IEntityObject obj, Expression<Func<TListEntity, TEntity>> property, Expression<Func<TEntity, TListEntity>> backProperty, Expression<Func<TEntity, TListEntity, bool>> keyMatching)
        {
            return null;
        }

        public static Func<object, object, bool> MultiMap<TEntity, TListEntity>(this IEntityObject obj, Expression<Func<TListEntity, IEnumerable<TEntity>>> property, Expression<Func<TEntity, IEnumerable<TListEntity>>> backProperty, Expression<Func<TEntity, TListEntity, bool>> keyMatching)
        {
            return null;
        }

        public static Func<object, object, bool> MultiMap<TEntity, TListEntity>(this IEntityObject obj, Expression<Func<TListEntity, TEntity>> property, Expression<Func<TEntity, IEnumerable<TListEntity>>> backProperty, Expression<Func<TEntity, TListEntity, bool>> keyMatching)
        {
            return null;
        }

        public static Func<object, object, bool> MultiMap<TEntity, TListEntity>(this IEntityObject obj, Expression<Func<TListEntity, TEntity>> property, Expression<Func<TEntity, TListEntity, bool>> keyMatching)
        {
            return null;
        }

        public static Func<object, object, bool> MultiMap<TEntity, TListEntity>(this IEntityObject obj, Expression<Func<TListEntity,  IEnumerable<TEntity>>> property, Expression<Func<TEntity, TListEntity, bool>> keyMatching)
        {
            return null;
        }
	}

    internal static class ExpressionExtensions
	{
		public static string MemberName<T, TProperty>(this Expression<Func<T, TProperty>> expression)
		{
			return expression.Body.MemberName();
		}

		public static PropertyInfo PropertyInfo(this Expression expression)
		{
			var memberExpression = expression as MemberExpression;

			if (expression.NodeType == ExpressionType.Convert)
			{
				memberExpression = ((UnaryExpression)expression).Operand as MemberExpression;
			}
			else if (expression.NodeType == ExpressionType.MemberAccess)
			{
				memberExpression = expression as MemberExpression;
			}

			if (memberExpression == null)
			{
				throw new InvalidOperationException("Expression must be a member expression");
			}

			if (memberExpression.Member.MemberType != MemberTypes.Property)
			{
				throw new InvalidOperationException("Expression has to provide access to property");
			}

			return memberExpression.Member as PropertyInfo;
		}
		
		public static string MemberName(this Expression expression)
		{
			return PropertyInfo(expression).Name;
		}

		public static TAttribute GetAttribute<TAttribute, TModel, TProperty>(this Expression<Func<TModel, TProperty>> expression)
				where TAttribute : Attribute
		{
			var memberExpression = expression.Body as MemberExpression;
			if (memberExpression == null)
				throw new InvalidOperationException("Expression must be a member expression");

			var attributes = memberExpression.Member.GetCustomAttributes(typeof(TAttribute), true);
			return attributes.Length > 0 ? attributes[0] as TAttribute : null;
		}

	}

    internal static class TypeExtensions
    {
        public static bool IsSimpleType(this Type type)
        {
            return type.IsValueType || (type == typeof(string));
        }

        public static bool IsEnumerable(this Type type)
        {
            return typeof(IEnumerable).IsAssignableFrom(type);
        }

        public static bool IsNullable(this Type type)
        {
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
        }
    }

    internal class Converter
    {
        static public object ChangeType(object value, Type conversionType)
        {
            return value == null ? null : ChangeType(value, value.GetType(), conversionType);
        }

        static public object ChangeTypeGeneric<TFrom, TTo>(object value)
        {
            return ChangeType(value, typeof(TFrom), typeof(TTo));
        }

        static public object ChangeType(object value, Type typeFrom, Type typeTo)
        {
            if (value == null)
            {
                return null;
            }

            if (typeTo.IsEnum)
            {
                return ConvertToEnum(value, typeFrom, typeTo);
            }

            if (typeFrom.IsEnum)
            {
                return typeTo == typeof(string) ? value.ToString() : ChangeType(Convert.ChangeType(value, Enum.GetUnderlyingType(typeFrom)), Enum.GetUnderlyingType(typeFrom), typeTo);
            }

            if (typeTo == typeof(Guid))
            {
                return new Guid(value.ToString());
            }

            var isFromNullable = typeFrom.IsNullable();
            var isToNullable = typeTo.IsNullable();

            if (isFromNullable && !isToNullable)
            {
                return ChangeType(value, Nullable.GetUnderlyingType(typeFrom), typeTo);
            }

            if (isToNullable)
            {
                var ut = Nullable.GetUnderlyingType(typeTo);
                return ut.IsEnum ? ConvertToEnum(value, typeFrom, ut) : ChangeType(value, typeFrom, ut);
            }

            return Convert.ChangeType(value, typeTo);
        }

        public static string ObjectToString(object obj)
        {
            if (obj == null)
            {
                return null;
            }
            return obj.ToString();
        }

        public static Guid StringToGuid(string str)
        {
            if (string.IsNullOrEmpty(str))
            {
                return Guid.Empty;
            }
            return new Guid(str);
        }

        internal static TEnum ToEnum<TEnum, TUnder>(object obj)
        {
            if (obj is string)
            {
                var str = obj.ToString();
                return (TEnum)Enum.Parse(typeof(TEnum), str);
            }
            return (TEnum)Convert.ChangeType(obj, typeof(TUnder));
        }

        public static MethodInfo GetConversionMethod(Type from, Type to)
        {
            if (from == null || to == null)
            {
                return null;
            }

            if (to == typeof(string))
            {
                return typeof(Converter).GetMethod("ObjectToString", BindingFlags.Static | BindingFlags.Public);
            }

            if (to.IsEnum)
            {
                return typeof(Converter)
                    .GetMethod("ToEnum", BindingFlags.Static | BindingFlags.NonPublic)
                    .MakeGenericMethod(to, Enum.GetUnderlyingType(to));
            }

            if (IsComplexConvert(from) || IsComplexConvert(to))
            {
                return
                    typeof(Converter)
                        .GetMethod(
                            "ChangeTypeGeneric",
                            BindingFlags.Static | BindingFlags.Public
                        )
                        .MakeGenericMethod(from, to);
            }
            return null;
        }

        private static bool IsComplexConvert(Type type)
        {
            if (type.IsEnum)
            {
                return true;
            }
            if (type.IsNullable())
            {
                if (Nullable.GetUnderlyingType(type).IsEnum)
                {
                    return true;
                }
            }
            return false;
        }

        static private object ConvertToEnum(object value, Type typeFrom, Type typeTo)
        {
            if (!typeFrom.IsEnum)
            {
                if (typeFrom == typeof(string))
                {
                    return Enum.Parse(typeTo, value.ToString());
                }
            }
            return Enum.ToObject(typeTo, Convert.ChangeType(value, Enum.GetUnderlyingType(typeTo)));
        }
    }
}